package com.company.bookstore.schedule;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.company.bookstore.common.result.MvcResult;
import com.company.bookstore.entities.book_module.Book;
import com.company.bookstore.entities.order_module.Order;
import com.company.bookstore.entities.user_admin_module.Schedule;
import com.company.bookstore.service.book_module.BookService;
import com.company.bookstore.service.user_admin_module.ScheduleService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.lang.Nullable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 管理员开启活动，type 有多种活动
 * 参数符合cron 表达式
 * 功能 ： 管理员开关定时任务
 *
 * 1. 管理员点击添加定时任务 ，传入cron 参数
 * 2. 后端通过判断该定时任务是否已经创建，若已经创建（说明有一台机器已经开启了这个定时任务）（使用redis作缓存处理）
 * 3. 若没有创建，则新建sql的一条记录，并在redis的map集合中 key 为 类名+前缀， value 为是否启动
 *
 * 如何实现一个秒杀活动的定时启动与结束
 * 1. 如何实现动态添加秒杀的接口地址
 *   提前设置好Controller 权限为user权限
 * 2. 如何配置他的参数 秒杀书本，秒杀价格，cron，持续时间
 * 3. 在job中 实现 将秒杀的书本载入到缓存中，以此作为该书本的秒杀标志
 * 4. 用户点击秒杀按钮获取url ，目的是秒杀活动之前秒杀接口不进行暴露，保存为url_userid，0
 * 5. 访问秒杀接口，提供url与userid，如果大于=1 则为重复秒杀
 * 6. 如果等于0，则允许秒杀
 * 7. 判断本地秒杀副本 若为false 则返回
 * 8  进行图书数量减少操作，并获取图书数量剩余数量作本地秒杀判断
 * 9. 将剩余的数量，与订单信息一起发给消费者，等待返回
 * Redis缓存和MySQL数据一致性3种方案详解
 * https://blog.csdn.net/qq_2300688967/article/details/96600577

 *
 * 为什么只能运行一个实例？，同一个类的多个实例只能运行一个？ enddate已过期
 *
 * 高并发 --》 采用jvm 内存队列+ 多台机器
 *
 *
 * */

@RestController
@PreAuthorize(value = "hasRole('ROLE_ADMIN')")
@RequestMapping("/schedule")
@Slf4j
public class ScheduleController  {
    public static final String DEFAULT_GROUP = "default";
    public static final Schedule EMPTY_SCHEDULE = new Schedule();
//    public static final Pattern CRON_PATTERN  = Pattern.compile( "(((^([0-9]|[0-5][0-9])(\\,|\\-|\\/){1}([0-9]|[0-5][0-9]) )|" +
//            "^([0-9]|[0-5][0-9]) |" +
//            "^(\\* ))((([0-9]|[0-5][0-9])(\\,|\\-|\\/){1}([0-9]|[0-5][0-9]) )|" +
//            "([0-9]|[0-5][0-9]) |" +
//            "(\\* ))((([0-9]|[01][0-9]|2[0-3])(\\,|\\-|\\/){1}([0-9]|[01][0-9]|2[0-3]) )|" +
//            "([0-9]|[01][0-9]|2[0-3]) |" +
//            "(\\* ))((([0-9]|[0-2][0-9]|3[01])(\\,|\\-|\\/){1}([0-9]|[0-2][0-9]|3[01]) )|(([0-9]|[0-2][0-9]|3[01]) )|" +
//            "(\\? )|(\\* )|(([1-9]|[0-2][0-9]|3[01])L )|([1-7]W )|(LW )|([1-7]\\#[1-4] ))((([1-9]|0[1-9]|1[0-2])(\\,|\\-|\\/){1}([1-9]|0[1-9]|1[0-2]) )|([1-9]|0[1-9]|1[0-2]) |" +
//            "(\\* ))(([1-7](\\,|\\-|\\/){1}[1-7])|([1-7])|(\\?)|(\\*)|(([1-7]L)|([1-7]\\#[1-4]))))|" +
//            "(((^([0-9]|[0-5][0-9])(\\,|\\-|\\/){1}([0-9]|[0-5][0-9]) )|^([0-9]|[0-5][0-9]) |^(\\* ))((([0-9]|[0-5][0-9])(\\,|\\-|\\/){1}([0-9]|[0-5][0-9]) )|([0-9]|[0-5][0-9]) |" +
//            "(\\* ))((([0-9]|[01][0-9]|2[0-3])(\\,|\\-|\\/){1}([0-9]|[01][0-9]|2[0-3]) )|([0-9]|[01][0-9]|2[0-3]) |(\\* ))((([0-9]|[0-2][0-9]|3[01])(\\,|\\-|\\/){1}([0-9]|[0-2][0-9]|3[01]) )|" +
//            "(([0-9]|[0-2][0-9]|3[01]) )|(\\? )|(\\* )|(([1-9]|[0-2][0-9]|3[01])L )|([1-7]W )|(LW )|([1-7]\\#[1-4] ))((([1-9]|0[1-9]|1[0-2])(\\,|\\-|\\/){1}([1-9]|0[1-9]|1[0-2]) )|([1-9]|0[1-9]|1[0-2]) |" +
//            "(\\* ))(([1-7](\\,|\\-|\\/){1}[1-7] )|([1-7] )|(\\? )|(\\* )|(([1-7]L )|([1-7]\\#[1-4]) ))((19[789][0-9]|20[0-9][0-9])\\-(19[789][0-9]|20[0-9][0-9])))");

    public final static Map<String,Schedule> concurrentHashMap = new ConcurrentHashMap<String, Schedule>();
    public final static HashMap<Integer,String> bookNameCache = new HashMap<>();
//    管理秒杀任务的启动和暂停  只需要一个scheduler 一个job 对于一个 任务  虽然可以接受多个触发器，暂不考虑 触发器加job对应一条sql记录，一个组内的命名需要唯一，暂不考虑组
//    private final static Map<String, Scheduler> secondSkillMap = new ConcurrentHashMap<String, Scheduler>();
    private static Scheduler SCHEDULER;

    static {
        try {
            SCHEDULER = StdSchedulerFactory.getDefaultScheduler();
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }


    @Resource
    ScheduleService scheduleService;
    @Resource
    BookService bookService;




    public static void removeTask(String name)  {
        concurrentHashMap.remove(name);
        try {
            SCHEDULER.deleteJob(JobKey.jobKey(name,DEFAULT_GROUP));
        } catch (SchedulerException e) {
            log.info("Method ：removeTask ==》 删除失败");
            e.printStackTrace();
        }
    }

    public static void registerTask(List<Schedule> list) throws SchedulerException {
        final Date date = new Date();
        list.forEach(
                v->{
                    try {
                        //                    通过数据校验唯一性后，应用启动后载入
                        concurrentHashMap.put(v.getName(),v);
                        JobDetail jobDetail = MiaoShaSchedule.createJobDetail(MiaoShaJob.class,v.getName(),
                                DEFAULT_GROUP,v.getFkBookId()
                        );
                        CronTrigger cronTrigger = MiaoShaSchedule.createCronTrigger(v.getStartTime(),
                                v.getEndTime(),v.getName(),DEFAULT_GROUP,v.getCorn()
                                ,jobDetail);
//                        Scheduler scheduler = MiaoShaSchedule.createSchedule(jobDetail,cronTrigger);
                        SCHEDULER.scheduleJob(jobDetail,cronTrigger);
                        if (v.getStatus()==1){
//                            scheduler.start();
                            if (v.getStartTime().compareTo(date)<=0&&v.getEndTime().compareTo(date)>=0){
                                log.info("job {} start",jobDetail);
                            }else {
                                log.info("job {} out of date",jobDetail);
                            }
                        }else {
//                            SCHEDULER.pauseJob(JobKey.jobKey(v.getName(),DEFAULT_GROUP));
                            SCHEDULER.pauseTrigger(TriggerKey.triggerKey(v.getName(),DEFAULT_GROUP));
                            log.info("job {} pause",jobDetail);
                        }
//                        secondSkillMap.put(v.getName(),scheduler);
                    } catch (SchedulerException e) {
                        e.printStackTrace();
                    }

                }

        );
        SCHEDULER.start();
    }
//    private static Scheduler createTask(Schedule v) throws SchedulerException {
//        return MiaoShaSchedule.createSchedule(MiaoShaSchedule.createJobDetail(MiaoShaJob.class,v.getName(),DEFAULT_GROUP,v.getFkBookId()), MiaoShaSchedule.createCronTrigger(v.getStartTime(),v.getEndTime(),v.getName(),DEFAULT_GROUP,v.getCorn()));
//    }




    private void getTaskStatus() throws SchedulerException {
        log.info("TaskStatus: {}",SCHEDULER.getCurrentlyExecutingJobs());
    }
    /**
     * 先添加数据库，再添加缓存
     * 应用宕机 --》 重启时会加载 数据库里的数据
     * 数据库宕机 --》概率较低，直接返回失败
     *{
     *     "name":"测试任务2",
     *     "corn":"0/30 * * * * ?",  ---》 要大于10s
     *     "startTime":"2020-09-24 15:00:00",
     *     "endTime":"2020-09-24 22:00:00",
     *     "fkBookId":"6"
     *     "second" : "20" 如果是秒级，要小于触发周期 如果是分钟级，也要触发周期大于持续时间
     *
     *     暂时不考虑触发周期和 持续时间问题  默认 每30秒触发一次 每次持续10 秒 默认 2s轮询一次 每次静止2s 存活4s
     *
     *     如何判断秒杀书本列表
     *     任务触发后，说明秒杀书本开始，汪redis 加入秒杀书本的集合，秒杀结束后，将集合中的书本删除
     * }
     * */
    @PostMapping("")
    public MvcResult<Object> addSchedule(@RequestBody  Schedule schedule) throws SchedulerException {
        if (schedule.getId() != null){
            return MvcResult.builder().code(400).message("不能指定任务").build();
        }
        if (schedule.getStatus()!=null){
//            0 就绪  1是 启动
            return MvcResult.builder().code(400).message("不能指定任务状态").build();
        }

        TriggerBuilder<Trigger> triggerTriggerBuilder = TriggerBuilder.newTrigger();
        JobBuilder jobJobBuilder = JobBuilder.newJob(MiaoShaJob.class);
        if (schedule.getName()!=null){
            if ( scheduleService.getOne(new QueryWrapper<Schedule>().eq("name",schedule.getName()))!=null){
                return MvcResult.builder().code(400).message("任务名字重复").build();
            }
        }else {
            return MvcResult.builder().code(400).message("需要指定任务名字").build();
        }
        JobKey jobKey = JobKey.jobKey(schedule.getName(),DEFAULT_GROUP);
        jobJobBuilder.withIdentity(jobKey);
        triggerTriggerBuilder.forJob(jobKey).withIdentity(schedule.getName(),DEFAULT_GROUP);
        if (schedule.getCorn()!=null){
//            Matcher matcher = CRON_PATTERN.matcher(schedule.getCorn());
//            if (! matcher.matches()){
//                return MvcResult.builder().code(400).message("CRON 表达式格式不规范").build();
//            }
        }else {
            return MvcResult.builder().code(400).message("需要指定任务执行时间").build();
        }
        triggerTriggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(schedule.getCorn()).withMisfireHandlingInstructionDoNothing());

        if (schedule.getFkBookId()!=null) {
            Book book =  bookService.getById(schedule.getFkBookId());
            if (book == null){
                return MvcResult.builder().code(400).message("没有该书籍").build();
            }
            if (book.getRepositoryNum() == 0){
                return MvcResult.builder().code(400).message("书本已经卖完").build();
            }
            if (scheduleService.getOne(new QueryWrapper<Schedule>().eq("fk_book_id",schedule.getFkBookId()))!=null)
            {
                return MvcResult.builder().code(400).message("该书本已经在拍卖中").build();
            }
        }else if(schedule.getBookName()!=null){
            Book book =  bookService.getOne(new QueryWrapper<Book>().eq("book_name",schedule.getBookName()));
            if (book == null){
                return MvcResult.builder().code(400).message("没有该书籍").build();
            }
            schedule.setFkBookId(book.getId());
        }else {
            return MvcResult.builder().code(400).message("需要指定图书").build();
        }

        jobJobBuilder.usingJobData("bookId",schedule.getFkBookId()).withDescription("秒杀图书为："+schedule.getFkBookId())
        .storeDurably(true).requestRecovery(false);
        if (schedule.getStartTime()!=null){
            triggerTriggerBuilder.startAt(schedule.getStartTime());
        }else {
            triggerTriggerBuilder.startAt(new Date());
        }
        triggerTriggerBuilder.endAt(schedule.getEndTime());
        schedule.setStatus(0);
//        可能出现数据库没有 但内存有数据的情况
        SCHEDULER.scheduleJob(jobJobBuilder.build(),triggerTriggerBuilder.build());
//        SCHEDULER.pauseTrigger(TriggerKey.triggerKey(schedule.getName(),DEFAULT_GROUP));
        SCHEDULER.pauseJob(jobKey);
        if (scheduleService.save(schedule)){
            concurrentHashMap.put(schedule.getName(),schedule);
            return MvcResult.builder().message("创建定时任务成功").code(200).build();
        }else {
            return MvcResult.builder().message("创建定时任务失败").code(500).build();
        }
    }
    /**
     * 当缓存中找不到数据时，去数据库拉取数据，并更行缓存
     * 当缓存命中时，直接返回
     * */
    @GetMapping("")
    public MvcResult<Object> getSchedule(@Nullable  @RequestParam Boolean isAll,@Nullable @RequestParam String taskName) throws SchedulerException {
//        getTaskStatus();
        if (isAll!=null&&isAll){
            final List<Schedule>  schedules = new ArrayList<>();
            final List<Integer> bookIds =  new ArrayList<>();
            final List<String> names = new ArrayList<>();
                concurrentHashMap.forEach(
                        (k,v)->{
                            if (!bookNameCache.containsKey(v.getFkBookId())){
                                bookIds.add(v.getFkBookId());
                            }
//                            log.info("{} == {} ?",v,EMPTY_SCHEDULE);
                            if (!v.equals(EMPTY_SCHEDULE)){
                                schedules.add(v);
                            }else {
                                log.info("uncached {}",k);
                                names.add(k);
                            }
                        }
                );
                if (bookIds.isEmpty()){
                    log.info("bookName cached All");
                }else {
                    List<Book> bookVos = bookService.getNamesMapWithId(bookIds);
                    bookVos.forEach(
                            (k)->{
                                bookNameCache.put(k.getId(),k.getBookName());
                            }
                    );
                }
                if (names.size() == 0){
                    log.info("schedule cached All");
                }else {
                    List<Schedule> list = scheduleService.list(new QueryWrapper<Schedule>().in("name",names));
                    list.forEach(
                            v->{
                              concurrentHashMap.put(v.getName(), v);
                              log.info("flush cache {}",v.getName());
                            }
                    );
                    schedules.addAll(list);
                }
                schedules.forEach(
                        v->{
                            v.setBookName(bookNameCache.get(v.getFkBookId()));
                        }
                );
           return MvcResult.builder().code(200).message("获取任务列表").object(schedules).build();
         }else {
            if (taskName!=null){
                if (concurrentHashMap.get(taskName) == null){
                    Schedule schedule = scheduleService.getOne(new QueryWrapper<Schedule>().eq("name",taskName));
                    concurrentHashMap.put(schedule.getName(),schedule);
                    return MvcResult.builder().code(200).message("获取任务信息").object(schedule).build();
                }else {
                    return MvcResult.builder().code(200).message("获取任务信息").object(concurrentHashMap.get(taskName)).build();
                }
            }
        }
        return MvcResult.builder().code(400).message("暂时不提供其他功能").object(null).build();
    }
    /**
     * redis 和 mysql 谁先修改：
     * 一个缓存涉及的表的字段，在 1 分钟内就修改了 20 次，或者是 100 次，那么缓存更新 20 次、100 次；但是这个缓存在 1 分钟内只被读取了 1 次，有大量的冷数据。
     * 实际上，如果你只是删除缓存的话，那么在 1 分钟内，这个缓存不过就重新计算一次而已，开销大幅度降低，用到缓存才去算缓存。
     * 其实删除缓存，而不是更新缓存，就是一个 lazy 计算的思想，不要每次都重新做复杂的计算，不管它会不会用到，而是让它到需要被使用的时候再重新计算。
     *
     *
     * 双删
     * 非高并发
     * 结合双删策略+缓存超时设置，这样最差的情况就是在超时时间内数据存在不一致（但保持最终一致性），而且又增加了写请求的耗时（Sleep）。
     * 另外如果上亿流量高并发时，出现了复杂点的情况: 数据发生了变更，先删除了缓存，然后要去修改数据库，还没来得及修改，一个请求过来，去读缓存，发现缓存空了，去查询数据库，查到了修改前的旧数据，放到了缓存中。
     * 随后数据变更的程序完成了数据库的修改, 结果此时线程宕机了，没有删除掉缓存，则也会出现数据不一致情况。
     *
     *
     * 高并发：
     * https://blog.csdn.net/yang5726685/article/details/54234569
     * 不是很懂
     * 使用jvm 内存队列，将每次请求写入队列，使用多线程执行队列中的请求
     *
     *
     * 订阅binlog程序在mysql中有现成的中间件叫canal，可以完成订阅binlog日志的功能
     *
     *
     * ConcurrentHashMap为何不支持null键和null值
     * https://blog.csdn.net/u013837825/article/details/87743520
     *
     *
     * best demo
     * https://www.cnblogs.com/yuyu666/p/10064833.html
     * */
    @PutMapping("/status")
    public MvcResult<Object> updateScheduleStatus(@RequestParam Integer id,@RequestParam String name,@RequestParam Integer status) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(name,DEFAULT_GROUP);
        TriggerKey triggerKey = TriggerKey.triggerKey(name,DEFAULT_GROUP);

        if (!SCHEDULER.checkExists(jobKey)){
            return MvcResult.builder().code(400).message("没有该任务").build();
        }

        if (!SCHEDULER.checkExists(triggerKey)){
            return MvcResult.builder().code(400).message("没有该触发器").build();
        }

        if (status>=2||status<0){
            return MvcResult.builder().code(400).message("状态参数格式错误").build();
        }else if (status == 1){
            concurrentHashMap.put(name,EMPTY_SCHEDULE);
           if( scheduleService.updateById( Schedule.builder().id(id).status(status).build())){
               SCHEDULER.resumeJob(jobKey);
               SCHEDULER.resumeTrigger(triggerKey);
               concurrentHashMap.put(name,EMPTY_SCHEDULE);
               return MvcResult.builder().code(200).message("任务启动成功").build();
            }
        }else{
            concurrentHashMap.put(name,EMPTY_SCHEDULE);
            if( scheduleService.updateById( Schedule.builder().id(id).status(status).build())){
//                会恢复之前暂停的触发
                SCHEDULER.pauseTrigger(triggerKey);
                SCHEDULER.pauseJob(jobKey);
                concurrentHashMap.put(name,EMPTY_SCHEDULE);
                return MvcResult.builder().code(200).message("任务暂停成功").build();
            }
        }

        return MvcResult.builder().code(200).message("调整任务状态失败").build();
    }

    @PutMapping("/book")
    public MvcResult<Object> updateScheduleBook(@RequestBody Schedule schedule){
        if (schedule.getId() == null){
            return MvcResult.builder().code(200).message("没有指定任务").build();
        }
        if (schedule.getName() == null){
            return MvcResult.builder().code(200).message("没有指定任务名称").build();
        }

        try {
            if (schedule.getFkBookId()!=null){
                if (scheduleService.updateById(schedule)){
                    concurrentHashMap.put(schedule.getName(),EMPTY_SCHEDULE);
                    JobKey jobKey = JobKey.jobKey(schedule.getName(), DEFAULT_GROUP);
                    SCHEDULER.pauseJob(jobKey);
                    SCHEDULER.addJob(MiaoShaSchedule.createJobDetail(MiaoShaJob.class,schedule.getName(),DEFAULT_GROUP,schedule.getFkBookId()),true);
                    SCHEDULER.resumeJob(jobKey);
                }
                return MvcResult.builder().message("调整秒杀对象成功").code(200).build();
            }
        }catch (Exception e){
            return MvcResult.builder().message("调整秒杀对象失败").code(500).build();
        }
        return MvcResult.builder().message("调整秒杀对象失败").code(500).build();
    }

    @PutMapping("/time")
    public MvcResult<Object> updateScheduleTime(@RequestBody Schedule schedule) throws SchedulerException {
        if (schedule.getId() == null){
            return MvcResult.builder().code(200).message("没有指定任务").build();
        }
        if (schedule.getName() == null){
            return MvcResult.builder().code(200).message("没有指定任务名称").build();
        }
        String name = schedule.getName();
        Integer id = schedule.getId();
        Schedule scheduleUpdate = Schedule.builder().id(id).name(name).build();
        boolean flag = false;
            if (schedule.getStartTime()!=null){
                scheduleUpdate.setStartTime(schedule.getStartTime());
                flag = true;
            }
            if (schedule.getEndTime()!=null){
                scheduleUpdate.setEndTime(schedule.getEndTime());
                flag = true;
            }
            if (schedule.getCorn()!=null){
                scheduleUpdate.setCorn(schedule.getCorn());
                flag = true;
            }


            if (flag&&scheduleService.updateById(scheduleUpdate)){
                concurrentHashMap.put(name,EMPTY_SCHEDULE);
                TriggerKey triggerKey = TriggerKey.triggerKey(name,DEFAULT_GROUP);
                SCHEDULER.pauseTrigger(triggerKey);
                TriggerBuilder<Trigger> triggerBuilder  = TriggerBuilder.newTrigger().withIdentity(name, DEFAULT_GROUP).forJob(JobKey.jobKey(name,DEFAULT_GROUP)).startNow();

                if (schedule.getStartTime()!=null){
                   triggerBuilder.startAt(schedule.getStartTime());
                }
                if (schedule.getEndTime()!=null){
                    triggerBuilder.endAt(schedule.getEndTime());
                }
                if (schedule.getCorn()!=null){
                    triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(schedule.getCorn()).withMisfireHandlingInstructionDoNothing());
                }
                Trigger trigger = triggerBuilder.build();
                SCHEDULER.rescheduleJob(triggerKey,trigger);
                SCHEDULER.start();
                return MvcResult.builder().message("调整任务时间成功").code(200).build();
            }else {
                return MvcResult.builder().message("调整任务时间失败").code(500).build();
            }
    }

//   当book发生修改时 需要替换工作流    SCHEDULER.addJob();
//   当起始时间和结束时间 和触发时间，发生修改时       SCHEDULER.rescheduleJob()

    /**
     * redis 和 mysql 谁先删除：
     * 如果redis先删除 ，则可能在查询的时候 出现redis脏数据 需要一定的策略
     * 如果mysql先删除，会出现一定的延迟性 采取， 可以容忍 ，可能出现更新了缓存中的数据缺发现数据库没有数据而报错（取消修改，保留缓存中记录），或者 一段时间内无法添加新的同名任务（可以容忍）
     * */
    @DeleteMapping()
    public MvcResult<Object> deleteSchedule(@RequestParam Integer id) throws SchedulerException {
        Schedule schedule = scheduleService.getById(id);
        if (
                schedule == null
        ){
            return MvcResult.builder().code(500).message("任务不存在").build();
        }

        if (scheduleService.remove(new QueryWrapper<Schedule>().eq("name",schedule.getName()))){

            concurrentHashMap.remove(schedule.getName());
            SCHEDULER.unscheduleJob(TriggerKey.triggerKey(schedule.getName(),DEFAULT_GROUP));
            SCHEDULER.deleteJob(JobKey.jobKey(schedule.getName(),DEFAULT_GROUP));
            getTaskStatus();
            return MvcResult.builder().code(200).message("删除成功").build();
        }else{
            return MvcResult.builder().code(500).message("删除失败").build();
        }
    }


}
